//////////////////////////////////////////////////////////////////////////////////
// Who  : Lim, Dong-moon (dmlim@nate.com, 011-701-9381)
// When : 2006/??/??
//////////////////////////////////////////////////////////////////////////////////

#if !defined(WIN32) || defined(USE_OPENSSL)

#include <algorithm>
using namespace std;

#if defined(WIN32) && defined(USE_OPENSSL)
#pragma warning (disable: 4996)
#endif

#include <stdio.h>
#include <string.h>

#include <openssl/x509.h>
#include <openssl/x509v3.h>
#include <openssl/x509_vfy.h>
#include <openssl/bio.h>
#include <openssl/err.h>
#include <openssl/pem.h>
#include <openssl/evp.h>
#include <openssl/rand.h>
#include <openssl/rc2.h>

#include "ncrypt_openssl.h"
#include "crypt_util.h"
#include "ncrypt_wchar.h"
#include "base64.h"
#include "nmalloc.h"
#include "ncrypt_types.h"

#if OPENSSL_VERSION_NUMBER >= 0x1000000fL
#else
inline void X509_STORE_set_verify_cb(X509_STORE *ctx, int (*verify_cb)(int, X509_STORE_CTX *)) { ctx->verify_cb = verify_cb; }
#endif

int openssl_init_dummy = openssl_init();

typedef struct pw_cb_data
{
	const void *password;
	const char *prompt_info;
} PW_CB_DATA;

int password_callback(char *buf, int bufsiz, int verify, PW_CB_DATA *cb_tmp)
{
	int res = 0;
	const char *prompt_info = NULL;
	const char *password = NULL;
	PW_CB_DATA *cb_data = (PW_CB_DATA *)cb_tmp;
	
	if (cb_data)
	{
		if (cb_data->password)
			password = (const char*)cb_data->password;
		if (cb_data->prompt_info)
			prompt_info = cb_data->prompt_info;
	}
	
	if (password)
	{
		res = strlen(password);
		if (res > bufsiz)
			res = bufsiz;
		memcpy(buf, password, res);
		return res;
	}
	
	return res;
}

RSA* openssl_read_rsa(char* file, char* pass)
{
	FILE* fp = NULL;
	RSA* pKey;
	PW_CB_DATA cb_data;
	cb_data.password = pass;
	cb_data.prompt_info = file;
	TCHAR tmp[1024];

	if ( !(fp = fopen(file, "rt")) )
	{
		_stprintf(tmp, _T("Error in opening file : %s\n"), file);
		SET_ERROR_MESSAGE(tmp);
		return NULL;
	}

	if ( !(pKey = PEM_read_RSAPrivateKey(fp,NULL, (pem_password_cb *)password_callback, &cb_data)) )
	{
		SET_ERROR_MESSAGE(_T("Can not read Private Key File."));
		return NULL;
	}

	if ( fp ) fclose(fp);

	return pKey;
}

X509* openssl_read_cert(char* file)
{
	FILE* fp = NULL;
	X509* pCert;
	TCHAR tmp[1024];

	if ( !(fp = fopen(file, "rt")) )
	{
		_stprintf(tmp, _T("Error in opening file : %s\n"), file);
		SET_ERROR_MESSAGE(tmp);
		return NULL;
	}

	if ( !(pCert = PEM_read_X509(fp, NULL, NULL, NULL)) )
	{
		SET_ERROR_MESSAGE(_T("Can not read X509 Certification File."));
		return NULL;
	}

	if ( fp ) fclose(fp);

	return pCert;
}

int openssl_init()
{
	OpenSSL_add_all_algorithms();
	OpenSSL_add_all_ciphers();
	OpenSSL_add_all_digests();

	return 0;
}

int openssl_final()
{
	EVP_cleanup();
	RAND_cleanup();

	return 0;
}

#ifdef _TEST_ALG_

typedef struct _alg_map_st
{
    ALG_ID  wincrypt_id;
    ALG_ID  openssl_id;
} ALG_MAP;

ALG_MAP wincrypt_openssl_alg_map[] = 
{
	{ CALG_AES_256,     NID_aes_256_cbc  },
	{ CALG_AES_192,     NID_aes_192_cbc  },
	{ CALG_AES_128,     NID_aes_128_cbc  },
	{ CALG_3DES,        NID_des_ede3_cbc },
	{ CALG_3DES_112,    NID_des_ede3_cbc },
	{ CALG_DES,         NID_des_cbc      },
	{ CALG_RC4,         NID_rc4_40       },
//	{ CALG_RC2,         NID_rc2_cbc      },
//	{ CALG_RC2,         NID_rc2_ecb      },
//	{ CALG_RC2,         NID_rc2_cfb64    },
//	{ CALG_RC2,         NID_rc2_ofb64    },
	{ CALG_RC2,         NID_rc2_40_cbc   },
//	{ CALG_RC2,         NID_rc2_64_cbc   },
	{ CALG_DESX,        NID_desx_cbc     },
	{ CALG_RC5,         NID_rc5_cbc      },
	{ CALG_AES,         CALG_AES_128     },
};

int ALG_MS_2_OPENSSL(ALG_ID algid)
{
	for ( int i = 0 ; i < sizeof(wincrypt_openssl_alg_map)/sizeof(ALG_MAP); i++ )
	{
		if ( wincrypt_openssl_alg_map[i].wincrypt_id == algid )
			return wincrypt_openssl_alg_map[i].openssl_id;
	}

	return 0;
}

ALG_ID ALG_OPENSSL_2_MS(int nid)
{
	for ( int i = 0 ; i < sizeof(wincrypt_openssl_alg_map)/sizeof(ALG_MAP); i++ )
	{
		if ( wincrypt_openssl_alg_map[i].openssl_id == nid )
			return wincrypt_openssl_alg_map[i].wincrypt_id;
	}

	return 0;
}

#else

int ALG_MS_2_OPENSSL(ALG_ID algid)
{
	switch (algid)
	{
	case CALG_AES_256:	return NID_aes_256_cbc;
	case CALG_AES_192:	return NID_aes_192_cbc;
	case CALG_AES_128:	return NID_aes_128_cbc;
	case CALG_3DES: 	return NID_des_ede3_cbc;
	case CALG_3DES_112: return NID_des_ede3_cbc;
	case CALG_DES:		return NID_des_cbc;
	case CALG_RC4:		return NID_rc4_40;
	case CALG_RC2:		return NID_rc2_cbc;
	case CALG_DESX:		return NID_desx_cbc;
	case CALG_RC5:		return NID_rc5_cbc;
	case CALG_AES:		return NID_aes_128_cbc;
	case CALG_MD2:		return NID_md2;
	case CALG_MD5:		return NID_md5;
	case CALG_SHA1:		return NID_sha1;
	default:			return 0;
	}
}

ALG_ID ALG_OPENSSL_2_MS(int nid)
{
	switch (nid)
	{
	case NID_aes_256_cbc:	return CALG_AES_256;
	case NID_aes_192_cbc:	return CALG_AES_192;
	case NID_aes_128_cbc:	return CALG_AES_128;
	case NID_des_ede3_cbc:	return CALG_3DES;
	case NID_des_cbc:		return CALG_DES;
	case NID_rc4_40:		return CALG_RC4;
	case NID_rc2_cbc:		return CALG_RC2;
	case NID_desx_cbc:		return CALG_DESX;
	case NID_rc5_cbc:		return CALG_RC5;
	case NID_md2:			return CALG_MD2;
	case NID_md5:	 		return CALG_MD5;
	case NID_sha:			return CALG_SHA;
	case NID_sha1:			return CALG_SHA1;
	default:				return 0;
	}
}

#endif // _TEST_ALG_

void openssl_GetSupportedAlgs(
	const ALG_ID* pPreferAlgs, 
	DWORD cPreferLen,
	ALG_ID** ppSupportAlgs, 
	DWORD* pcSupportLen
)
{
	int i = 0;
	DWORD cnt = 0;
	*pcSupportLen = 0;

	for ( i = 0 ; i < (int)cPreferLen ; i++ )
	{
		if ( EVP_get_cipherbynid(ALG_MS_2_OPENSSL(pPreferAlgs[i])) )
		{
			cnt++;
		}
	}

	*ppSupportAlgs = (ALG_ID*)malloc(sizeof(ALG_ID)*cnt);

	for ( i = 0 ; i < (int)cPreferLen ; i++ )
	{
		if ( EVP_get_cipherbynid(ALG_MS_2_OPENSSL(pPreferAlgs[i])) )
		{
			(*ppSupportAlgs)[(*pcSupportLen)++] = pPreferAlgs[i];
		}
	}
}

BOOL openssl_ReadPEMPrivateKey(
	LPCTSTR lpszPKFile, 
	LPCSTR lpszPasswd, 
	RSA** ppRSAKey)
{
	FILE* fp = NULL;
	PW_CB_DATA cb_data;
	TCHAR tmp[1024];
	BOOL bReturn = FALSE;

	cb_data.password = lpszPasswd;
#if defined(WIN32) && defined(_UNICODE)
	char mbcsFileName[MAX_PATH];
	WideCharToMultiByte(CP_ACP, 0, lpszPKFile, -1, mbcsFileName, sizeof(mbcsFileName), 0, 0);
	cb_data.prompt_info = mbcsFileName;
#else
	cb_data.prompt_info = lpszPKFile;
#endif

	if ( !(fp = _tfopen(lpszPKFile, _T("r"))) )
	{
		_stprintf(tmp, _T("Error in opening file : %s\n"), lpszPKFile);
		SET_ERROR_MESSAGE(tmp);
		goto cleanup;
	}

	if ( !(*ppRSAKey = PEM_read_RSAPrivateKey(fp,NULL, (pem_password_cb *)password_callback, &cb_data)) )
	{
		_stprintf(tmp, _T("Error in opening file : %s\n"), lpszPKFile);
		SET_ERROR_MESSAGE(tmp);
		goto cleanup;
	}

	bReturn = TRUE;

cleanup:

	if ( fp ) fclose(fp);
	return bReturn;
}

BOOL openssl_HashRandomAndSessionKey( 
	DWORD cbRandom,
	const BYTE* pbRandom,
	DWORD cbSessionKey,
	const BYTE* pbSessionKey,
	DWORD* pcbHash,
	BYTE** ppbHash)
{
	DWORD cbHashData = 0;
	BYTE* pbHashData = NULL;
	EVP_MD_CTX* pMdCtx = NULL;
	
	cbHashData = cbRandom + cbSessionKey;
	pbHashData = (BYTE*)malloc(cbHashData);
	CopyMemory(pbHashData, pbRandom, cbRandom);
	CopyMemory(pbHashData + cbRandom, pbSessionKey, cbSessionKey);

	pMdCtx = EVP_MD_CTX_create();

	EVP_DigestInit(pMdCtx, EVP_md5());
	EVP_DigestUpdate(pMdCtx, pbHashData, cbHashData);
	*pcbHash = EVP_MD_CTX_size(pMdCtx);
	*ppbHash = (BYTE*)malloc(*pcbHash);
	unsigned int cbHash = *pcbHash;
	EVP_DigestFinal(pMdCtx, *ppbHash, &cbHash);

	free(pbHashData);
	EVP_MD_CTX_destroy(pMdCtx);

	return TRUE;
}

BOOL openssl_MyEncrypt(
	const BYTE* pbSessionKey,
	ALG_ID cryptAlg,
	const BYTE* pbInData,
	DWORD dwInDataLen,
	BYTE** ppbOutData,
	DWORD* pdwOutDataLen)
{
	return openssl_MyEncrypt(pbSessionKey, NULL, cryptAlg, pbInData, dwInDataLen, ppbOutData, pdwOutDataLen);
}

BOOL openssl_MyDecrypt(
	const BYTE* pbSessionKey,
	ALG_ID cryptAlg,
	const BYTE* pbInData,
	DWORD dwInDataLen,
	BYTE** ppbOutData,
	DWORD* pdwOutDataLen)
{
	return openssl_MyDecrypt(pbSessionKey, NULL, cryptAlg, pbInData, dwInDataLen, ppbOutData, pdwOutDataLen);
}

BOOL openssl_MyHash(
	ALG_ID algId,
	const BYTE* pbDataIn,
	DWORD cbDataIn,
	BYTE** ppbHashOut,
	DWORD* pcbDataOut
	)
{
	const EVP_MD* pMd = 0;
	EVP_MD_CTX* pMdCtx = 0;
	BOOL bRet = FALSE;

	if ( !(pMd = EVP_get_digestbynid(ALG_MS_2_OPENSSL(algId))) )
	{
		SET_ERROR_MESSAGE(_T("Can not find hash algorithm."));
		goto cleanup;
	}

	pMdCtx = (EVP_MD_CTX*)malloc(sizeof(EVP_MD_CTX));

	if ( !EVP_DigestInit(pMdCtx, pMd) )
	{
		SET_ERROR_MESSAGE(_T("Can not create hash."));
		goto cleanup;
	}

	if ( !EVP_DigestUpdate(pMdCtx, pbDataIn, cbDataIn) )
	{
		SET_ERROR_MESSAGE(_T("Can not hash data."));
		goto cleanup;
	}

	*pcbDataOut = EVP_MD_size(pMd);
	*ppbHashOut = (BYTE*)malloc(*pcbDataOut);

	if ( !EVP_DigestFinal(pMdCtx, *ppbHashOut, 0) )
	{
		SET_ERROR_MESSAGE(_T("Can not get hash value."));
		goto cleanup;
	}

	bRet = TRUE;

cleanup:

	if ( pMdCtx ) free(pMdCtx);

	return bRet;
}

BOOL openssl_MyEncrypt(
	const BYTE* pbSessionKey,
	const BYTE* pbIV,
	ALG_ID cryptAlg,
	const BYTE* pbInData,
	DWORD dwInDataLen,
	BYTE** ppbOutData,
	DWORD* pdwOutDataLen)
{
	BOOL bReturn = FALSE;
	const EVP_CIPHER* pCipher = EVP_get_cipherbynid(ALG_MS_2_OPENSSL(cryptAlg));
	EVP_CIPHER_CTX* pCtx = (EVP_CIPHER_CTX*)malloc(sizeof(EVP_CIPHER_CTX));
	EVP_CIPHER_CTX_init(pCtx);
	DWORD cbBuffer = dwInDataLen + EVP_CIPHER_block_size(pCipher);
	BYTE* pbBuffer = 0;
	DWORD cbTotal = 0;
	int tmp = 0;
	BYTE* pKey = 0;

	if ( !dwInDataLen )
	{
		SET_ERROR_MESSAGE(_T("The size of data to be encryptd is zero"));
		goto cleanup;
	}

	if ( cryptAlg == CALG_3DES_112 )
	{
		pKey = (BYTE*)malloc(EVP_CIPHER_key_length(pCipher));
		int deslen = EVP_CIPHER_key_length(EVP_des_cbc());
		CopyMemory(pKey, pbSessionKey, deslen*2);
		CopyMemory(pKey + deslen*2, pbSessionKey, deslen);
	}
	else
	{
		pKey = (BYTE*)pbSessionKey;
	}

	if ( !EVP_EncryptInit(pCtx, pCipher, pKey, pbIV) )
	{
		SET_ERROR_MESSAGE(_T("Cannot initialize encryption."));
		goto cleanup;
	}

	pbBuffer = (BYTE*)malloc(cbBuffer);

	if ( !EVP_EncryptUpdate(pCtx, pbBuffer, &tmp, pbInData, dwInDataLen) )
	{
		SET_ERROR_MESSAGE(_T("Cannot update encryption."));
		goto cleanup;
	}

	cbTotal += tmp;

	if ( !EVP_EncryptFinal(pCtx, pbBuffer + cbTotal, &tmp) )
	{
		SET_ERROR_MESSAGE(_T("Cannot finalize encryption."));
		goto cleanup;
	}

	cbTotal += tmp;

	*pdwOutDataLen = cbTotal;
	*ppbOutData = pbBuffer;

	bReturn = TRUE;

cleanup:

	if ( cryptAlg == CALG_3DES_112 && pKey )
	{
		free(pKey);
	}

	if ( pCtx ) 
	{
		EVP_CIPHER_CTX_cleanup(pCtx);
		free(pCtx);
	}

	if ( !bReturn && pbBuffer )
	{
		free(pbBuffer);
	}

	return bReturn;
}

BOOL openssl_MyDecrypt(
	const BYTE* pbSessionKey,
	const BYTE* pbIV,
	ALG_ID cryptAlg,
	const BYTE* pbInData,
	DWORD dwInDataLen,
	BYTE** ppbOutData,
	DWORD* pdwOutDataLen)
{
	BOOL bReturn = FALSE;
	const EVP_CIPHER* pCipher = EVP_get_cipherbynid(ALG_MS_2_OPENSSL(cryptAlg));
	EVP_CIPHER_CTX* pCtx = (EVP_CIPHER_CTX*)malloc(sizeof(EVP_CIPHER_CTX));
	EVP_CIPHER_CTX_init(pCtx);
	DWORD cbBuffer = dwInDataLen + EVP_CIPHER_block_size(pCipher);
	BYTE* pbBuffer = 0;
	DWORD cbTotal = 0;
	int tmp = 0;
	BYTE* pKey = 0;

	if ( !dwInDataLen )
	{
		SET_ERROR_MESSAGE(_T("The size of data to be decryptd is zero"));
		goto cleanup;
	}

	if ( cryptAlg == CALG_3DES_112 )
	{
		pKey = (BYTE*)malloc(EVP_CIPHER_key_length(pCipher));
		int deslen = EVP_CIPHER_key_length(EVP_des_cbc());
		CopyMemory(pKey, pbSessionKey, deslen*2);
		CopyMemory(pKey + deslen*2, pbSessionKey, deslen);
	}
	else
	{
		pKey = (BYTE*)pbSessionKey;
	}

	if ( !EVP_DecryptInit(pCtx, pCipher, pKey, pbIV) )
	{
		SET_ERROR_MESSAGE(_T("Cannot initialize decryption."));
		goto cleanup;
	}

#ifdef _TEST_ALG_

	// TEST FOR RC2

	if ( cryptAlg == CALG_RC2 )
	{
		int len = 0;
		EVP_CIPHER_CTX_ctrl(pCtx, EVP_CTRL_SET_RC2_KEY_BITS, 5, NULL);
	}

#endif // _TEST_ALG_

	pbBuffer = (BYTE*)malloc(cbBuffer);

	if ( !EVP_DecryptUpdate(pCtx, pbBuffer, &tmp, pbInData, dwInDataLen) )
	{
		SET_ERROR_MESSAGE(_T("Cannot update decryption."));
		goto cleanup;
	}

	cbTotal += tmp;

	if ( !EVP_DecryptFinal(pCtx, pbBuffer + cbTotal, &tmp) )
	{
		SET_ERROR_MESSAGE(_T("Cannot finalize decryption."));
		goto cleanup;
	}

	cbTotal += tmp;

	*pdwOutDataLen = cbTotal;
	*ppbOutData = pbBuffer;

	bReturn = TRUE;

cleanup:

	if ( cryptAlg == CALG_3DES_112 && pKey )
	{
		free(pKey);
	}

	if ( pCtx ) 
	{
		EVP_CIPHER_CTX_cleanup(pCtx);
		free(pCtx);
	}

	if ( !bReturn && pbBuffer )
	{
		free(pbBuffer);
	}

	return bReturn;
}

BOOL openssl_CHashSaltPswd(
	const BYTE* pbSalt,
	DWORD cbSalt,
	LPCTSTR passwd,
	BYTE** ppbHash,
	DWORD* pcbHash
	)
{
#if defined(WIN32) && defined(_UNICODE)
	BYTE* pbPasswd = (BYTE*)passwd;
#else
	BYTE* pbPasswd = (BYTE*)UTF8toUTF16(passwd);
#endif
	DWORD cbPasswd = _wcslen((WCHAR*)pbPasswd) * sizeof(WCHAR);
	const EVP_MD* pMd = 0;
	EVP_MD_CTX* pMdCtx = 0;
	BOOL bSuccess = FALSE;

	*ppbHash = 0;
	*pcbHash = 0;

	if ( !(pMd = EVP_get_digestbynid(ALG_MS_2_OPENSSL(CALG_SHA1))) )
	{
		SET_ERROR_MESSAGE(_T("Can not find hash algorithm."));
		goto cleanup;
	}

	pMdCtx = (EVP_MD_CTX*)malloc(sizeof(EVP_MD_CTX));

	if ( !EVP_DigestInit(pMdCtx, pMd) )
	{
		SET_ERROR_MESSAGE(_T("Can not create hash."));
		goto cleanup;
	}

	if ( !EVP_DigestUpdate(pMdCtx, pbPasswd, cbPasswd) )
	{
		SET_ERROR_MESSAGE(_T("Can't hash password for salt and password"));
		goto cleanup;
	}

	if ( !EVP_DigestUpdate(pMdCtx, pbSalt, cbSalt) )
	{
		SET_ERROR_MESSAGE(_T("Can't hash salt for salt and password"));
		goto cleanup;
	}

	*pcbHash = EVP_MD_size(pMd);
	*ppbHash = (BYTE*)malloc(*pcbHash);

	if ( !EVP_DigestFinal(pMdCtx, *ppbHash, 0) )
	{
		SET_ERROR_MESSAGE(_T("Can't get hash value for salt and password"));
		goto cleanup;
	}

	bSuccess = TRUE;

cleanup:

	if ( pMdCtx ) free(pMdCtx);
	if ( pbPasswd ) free(pbPasswd);
	if ( !bSuccess && *ppbHash ) free(*ppbHash);

	return bSuccess;
}

BOOL openssl_CHashSaltPswd40(
	const BYTE* pbSalt,
	DWORD cbSalt,
	LPCTSTR passwd,
	BYTE** ppbHash,
	DWORD* pcbHash
	)
{
	BYTE* pbSaltHash = 0;
	DWORD cbSaltHash = 0;
	BYTE buff1[64];
	BYTE buff2[64];
	BYTE* pbHash3 = 0;
	DWORD cbHash3 = 0;
	BYTE* pbHash4 = 0;
	DWORD cbHash4 = 0;
	*ppbHash = 0;
	*pcbHash = 0;
	BOOL bSuccess = FALSE;
	int i = 0;

	for (i = 0 ; i < 64 ; i++)
	{
		buff1[i] = 0x36;
		buff2[i] = 0x5C;
	}

	if ( !openssl_CHashSaltPswd(pbSalt, cbSalt, passwd, &pbSaltHash, &cbSaltHash) )
	{
		SET_ERROR_MESSAGE(_T("Can't get salt and password hash"));
		goto cleanup;
	}

	// ----- Step 1; XOR first 20 bytes of buff1 with SHA1 hash (psalthash) of base data
	for (i = 0 ; i < 20 ; i++)
		buff1[i] ^= pbSaltHash[i];

	// ----- Step 2; XOR first 20 bytes of buff2 with SHA1 hash (psalthash) of base data
	for (i = 0 ; i < 20 ; i++)
		buff2[i] ^= pbSaltHash[i];
 
	// ----- Step 3; hash the entire first buffer
	if ( !openssl_MyHash(CALG_SHA1, buff1, sizeof(buff1), &pbHash3, &cbHash3) )
	{
		SET_ERROR_MESSAGE(_T("Can't get second hash for first buffer"));
		goto cleanup;
	}

	// ----- Step 4; hash the entire second buffer
	if ( !openssl_MyHash(CALG_SHA1, buff2, sizeof(buff2), &pbHash4, &cbHash4) )
	{
		SET_ERROR_MESSAGE(_T("Can't get second hash for second buffer"));
		goto cleanup;
	}

	*pcbHash = cbHash3 + cbHash4;
	*ppbHash = (BYTE*)malloc(*pcbHash);

	CopyMemory(*ppbHash, pbHash3, cbHash3);
	CopyMemory(*ppbHash + cbHash3, pbHash4, cbHash4);

	bSuccess = TRUE;

cleanup:

	if ( pbSaltHash ) free(pbSaltHash);
	if ( pbHash3 ) free(pbHash3);
	if ( pbHash4 ) free(pbHash4);

	return bSuccess;
}

BOOL openssl_MyDeriveKey(
	ALG_ID algId,
	const BYTE* pbSalt,
	DWORD cbSalt,
	LPCTSTR passwd,
	DWORD cbKey,
	BYTE** ppbKey
	)
{
	BOOL bSuccess = FALSE;
	DWORD dwSize = 0;

	if ( cbKey > 20 || 
		algId == CALG_AES_256 || 
		algId == CALG_AES_192 ||
		algId == CALG_AES_128 ||
		algId == CALG_AES     || 
		algId == CALG_3DES
		)
	{
		bSuccess = openssl_CHashSaltPswd40(pbSalt, cbSalt, passwd, ppbKey, &dwSize);
	}
	else
	{
		bSuccess = openssl_CHashSaltPswd(pbSalt, cbSalt, passwd, ppbKey, &dwSize);
	}

	return bSuccess;
}

#if OPENSSL_VERSION_NUMBER >= 0x10000000L
static void nodes_print(BIO *out, const char *name,
						STACK_OF(X509_POLICY_NODE) *nodes)
{
	X509_POLICY_NODE *node;
	int i;
	BIO_printf(out, "%s Policies:", name);
	if (nodes)
	{
		BIO_puts(out, "\n");
		for (i = 0; i < sk_X509_POLICY_NODE_num(nodes); i++)
		{
			node = sk_X509_POLICY_NODE_value(nodes, i);
			X509_POLICY_NODE_print(out, node, 2);
		}
	}
	else
	{
		BIO_puts(out, " <empty>\n");
	}
}
#endif 

#if OPENSSL_VERSION_NUMBER >= 0x10000000L
void policies_print(X509_STORE_CTX *ctx)
{
	X509_POLICY_TREE *tree;
	int explicit_policy;

	tree = X509_STORE_CTX_get0_policy_tree(ctx);
	explicit_policy = X509_STORE_CTX_get_explicit_policy(ctx);

	BIO* out = BIO_new(BIO_s_mem());

	BIO_printf(out, "Require explicit Policy: %s\n",
		explicit_policy ? "True" : "False");
	nodes_print(out, "Authority", X509_policy_tree_get0_policies(tree));
	nodes_print(out, "User", X509_policy_tree_get0_user_policies(tree));


	BUF_MEM *bptr;
	BIO_get_mem_ptr(out, &bptr);
	TCHAR* szMessage = 0;
#ifdef _UNICODE
	szMessage = (TCHAR*)malloc((bptr->length+1) * sizeof(TCHAR));
	int len = MultiByteToWideChar(CP_ACP, 0, bptr->data, bptr->length, szMessage, 1024);
	szMessage[len] = 0;
#else
	szMessage = (TCHAR*)malloc(bptr->length+1);
	int len = min(bptr->length, 1024-1);
	CopyMemory(szMessage, bptr->data, len);
	szMessage[len] = 0;
#endif

	SET_ERROR_MESSAGE(szMessage);

	if ( szMessage )
		free(szMessage);

	BIO_free(out);
}
#endif

int __cdecl verify_callback(int ok, X509_STORE_CTX *ctx)
{
	int cert_error = X509_STORE_CTX_get_error(ctx);
	X509 *current_cert = X509_STORE_CTX_get_current_cert(ctx);

	if (!ok)
	{
		if (current_cert)
		{
			BIO *out = BIO_new(BIO_s_mem());
			X509_NAME_print_ex(out, X509_get_subject_name(current_cert), 0, XN_FLAG_ONELINE);
			BUF_MEM *bptr;
			BIO_get_mem_ptr(out, &bptr);
			TCHAR szMessage[1024];
#ifdef _UNICODE
			int len = MultiByteToWideChar(CP_ACP, 0, bptr->data, bptr->length, szMessage, 1024);
			szMessage[len] = 0;
#else
			int len = min(bptr->length, 1024-1);
			CopyMemory(szMessage, bptr->data, len);
			szMessage[len] = 0;
#endif
			SET_ERROR_MESSAGE(szMessage);
			BIO_free(out);
		}

		{
			TCHAR errMessage[1024];
			TCHAR szMessage[1024];
#ifdef _UNICODE
			MultiByteToWideChar(CP_ACP, 0, X509_verify_cert_error_string(cert_error), -1, errMessage, 1024);
#else
			lstrcpy(errMessage, X509_verify_cert_error_string(cert_error));
#endif

			wsprintf(szMessage, _T("error %d at %d depth lookup: %s"),
				cert_error,
				X509_STORE_CTX_get_error_depth(ctx),
				errMessage);

			SET_ERROR_MESSAGE(szMessage);
		}

		switch(cert_error)
		{
#if OPENSSL_VERSION_NUMBER >= 0x10000000L
		case X509_V_ERR_NO_EXPLICIT_POLICY:
			policies_print(ctx);
#endif
		case X509_V_ERR_CERT_HAS_EXPIRED:

			/* since we are just checking the certificates, it is
			* ok if they are self signed. But we should still warn
			* the user.
			*/

		case X509_V_ERR_DEPTH_ZERO_SELF_SIGNED_CERT:
			/* Continue after extension errors too */
		case X509_V_ERR_INVALID_CA:
#if OPENSSL_VERSION_NUMBER >= 0x10000000L
		case X509_V_ERR_INVALID_NON_CA:
#endif
		case X509_V_ERR_PATH_LENGTH_EXCEEDED:
		case X509_V_ERR_INVALID_PURPOSE:
		case X509_V_ERR_CRL_HAS_EXPIRED:
		case X509_V_ERR_CRL_NOT_YET_VALID:
		case X509_V_ERR_UNHANDLED_CRITICAL_EXTENSION:
			break;
			//ok = 1;

		}

		return ok;

	}

#if OPENSSL_VERSION_NUMBER >= 0x10000000L
	if (cert_error == X509_V_OK && ok == 2)
		policies_print(ctx);
#endif

	ERR_clear_error();

	return(ok);
}

/*
MUST freed after using
sk_X509_pop_free(untrusted, X509_free)
*/

BOOL openssl_ReadCertFromMemory(BYTE* pData, DWORD cSize, X509** pCert)
{
	BOOL bResult = FALSE;
	BIO* pBio = BIO_new(BIO_s_mem());
	BIO_write(pBio, pData, cSize);

	if ( !(*pCert = d2i_X509_bio(pBio,NULL)) )
	{
		SET_ERROR_MESSAGE(_T("Can not read cert from bio."));
		goto cleanup;
	}

	bResult = TRUE;

cleanup:
	BIO_free(pBio);
	return bResult;
}

BOOL openssl_ReadCertFromFile(LPCTSTR filename, X509** pCert)
{
	BOOL bResult = FALSE;
	BYTE* pData = 0;
	DWORD cSize = 0;

	if ( !ReadPEMCertificate(filename, &pData, &cSize) )
	{
		TCHAR szMessage[1024];
		wsprintf(szMessage, _T("Can not read certficate from file: %s"), filename);
		SET_ERROR_MESSAGE(szMessage);
		goto cleanup;
	}

	if ( !openssl_ReadCertFromMemory(pData, cSize, pCert) )
	{
		SET_ERROR_MESSAGE(_T("Can not read certficates from memory."));
		goto cleanup;
	}

	bResult = TRUE;

cleanup:

	if ( pData )
	{
		free(pData);
	}

	return bResult;
}

BOOL openssl_ReadCertsFromMemory(int len, NBLOB* pBlobs, STACK_OF(X509)** pCerts)
{
	BOOL bResult = FALSE;
	int i = 0;

	if ( !(*pCerts = sk_X509_new_null()) )
	{
		SET_ERROR_MESSAGE(_T("Can not allocate new X509 stack"));
		goto cleanup;
	}

	for ( i = 0 ; i < len ; i++ )
	{
		X509* pCert = 0;

		if ( !openssl_ReadCertFromMemory(pBlobs[i].pData, pBlobs[i].cSize, &pCert) )
		{
			SET_ERROR_MESSAGE(_T("Can not read cert from memory."));
			goto cleanup;
		}

		if ( !sk_X509_push(*pCerts, pCert) )
		{
			SET_ERROR_MESSAGE(_T("Can not push x509 to stack."));
			goto cleanup;
		}
	}

	bResult = TRUE;

cleanup:

	if ( !bResult )
	{
		if ( *pCerts )
		{
			sk_X509_pop_free(*pCerts, X509_free);
			*pCerts = NULL;
		}
	}

	return bResult;
}

BOOL openssl_ReadCertsFromFile(LPCTSTR filename, STACK_OF(X509)** pCerts)
{
	BOOL bResult = FALSE;
	NBLOB blobs[10];
	ZeroMemory(blobs, sizeof(blobs));
	int len = sizeof(blobs) / sizeof(NBLOB);

	if ( !ReadPEMCertificates(filename, &len, blobs) )
	{
		TCHAR szMessage[1024];
		wsprintf(szMessage, _T("Can not read certficates from file: %s"), filename);
		SET_ERROR_MESSAGE(szMessage);
		goto cleanup;
	}

	if ( !openssl_ReadCertsFromMemory(len, blobs, pCerts) )
	{
		SET_ERROR_MESSAGE(_T("Can not read certficates from memory."));
		goto cleanup;
	}

	bResult = TRUE;

cleanup:

	for ( int i = 0 ; i < len ; i++ )
	{
		FreeBlob(&blobs[i]);
	}

	return bResult;
}

BOOL openssl_InsertCertsIntoStore(LPCTSTR filename, X509_STORE *pStore)
{
	BOOL bResult = FALSE;
	NBLOB blobs[10];
	ZeroMemory(blobs, sizeof(blobs));
	int len = sizeof(blobs) / sizeof(NBLOB);

	if ( !filename )
	{
		SET_ERROR_MESSAGE(_T("Certificate store file name is null"));
		goto cleanup;
	}

	if ( !ReadPEMCertificates(filename, &len, blobs) )
	{
		TCHAR szMessage[1024];
		wsprintf(szMessage, _T("Can not read certficates from file. %s"), filename);
		SET_ERROR_MESSAGE(szMessage);
		goto cleanup;
	}

	for ( int i = 0 ; i < len ; i++ )
	{
		X509* pCert = 0;
		if ( !openssl_ReadCertFromMemory(blobs[i].pData, blobs[i].cSize, &pCert) )
		{
			SET_ERROR_MESSAGE(_T("Can not read certficate from memory."));
			goto cleanup;
		}

		X509_STORE_add_cert(pStore, pCert);
	}

	bResult = TRUE;

cleanup:

	for ( int i = 0 ; i < len ; i++ )
	{
		FreeBlob(&blobs[i]);
	}

	return bResult;
}

LPCTSTR openssl_GetErrorMessage()
{
	static TCHAR szMessage[1024];
	BIO *out = BIO_new(BIO_s_mem());
	ERR_print_errors(out);
	BUF_MEM *bptr;
	BIO_get_mem_ptr(out, &bptr);
#ifdef _UNICODE
	int len = MultiByteToWideChar(CP_ACP, 0, bptr->data, bptr->length, szMessage, 1024);
	szMessage[len] = 0;
#else
	int len = min(bptr->length, 1024-1);
	CopyMemory(szMessage, bptr->data, len);
	szMessage[len] = 0;
#endif
	BIO_free(out);
	return szMessage;
}

BOOL openssl_VerifyHostName(X509 *cert, HostNameVerifyCallback fn, LPVOID ctx)
{
	char subjectCn[256];
	X509_get_subject_name(cert); 
	X509_NAME_get_text_by_NID(X509_get_subject_name(cert), NID_commonName, subjectCn, sizeof(subjectCn));

#ifdef _UNICODE
	WCHAR szTemp[256];
	MultiByteToWideChar(CP_ACP, 0, subjectCn, -1, szTemp, 256);
	return (*fn)(szTemp, ctx);
#else
	return (*fn)(subjectCn, ctx);
#endif
}

BOOL openssl_VerifyCertificate(NBLOB* pCert, int nChainsLen, NBLOB* pChains, LPCTSTR szCAFile, HostNameVerifyCallback fn, LPVOID ctx)
{
	BOOL bResult = FALSE;
	X509_STORE *store = 0;
	X509_STORE_CTX *csc = 0;
	STACK_OF(X509) *untrusted = 0;
	X509 *cert = 0;

	if ( !(store = X509_STORE_new()) ) 
	{
		SET_ERROR_MESSAGE(_T("Can not allocate new X509_STORE."));
		goto cleanup;
	}

	X509_STORE_set_verify_cb(store, verify_callback);

	if ( !openssl_InsertCertsIntoStore(szCAFile, store) )
	{
		SET_ERROR_MESSAGE(_T("Can not load root CA certficates file."));
		goto cleanup;
	}

	if ( !openssl_ReadCertFromMemory(pCert->pData, pCert->cSize, &cert) )
	{
		SET_ERROR_MESSAGE(_T("Can not load subject certificates file."));
		goto cleanup;
	}

	if ( nChainsLen > 0 )
	{
		if ( !openssl_ReadCertsFromMemory(nChainsLen, pChains, &untrusted) )
		{
			SET_ERROR_MESSAGE(_T("Can not load chain CA certificates from memory."));
			goto cleanup;
		}
	}

	if ( !(csc = X509_STORE_CTX_new()) )
	{
		SET_ERROR_MESSAGE(_T("Can not allocate X509_STORE_CTX."));
		goto cleanup;
	}

	if ( !X509_STORE_CTX_init(csc, store, cert, untrusted) )
	{
		SET_ERROR_MESSAGE(_T("Can not initialize X509_STORE_CTX."));
		goto cleanup;
	}

	if ( X509_verify_cert(csc) <= 0 )
	{
		SET_ERROR_MESSAGE(_T("Can not verify certificate chain."));
		goto cleanup;
	}

	if ( !fn )
	{
		SET_ERROR_MESSAGE(_T("Hostname Verifier is null"));
		goto cleanup;
	}

	if ( !openssl_VerifyHostName(cert, fn, ctx) )
	{
		SET_ERROR_MESSAGE(_T("Invalid Hostname."));
		goto cleanup;
	}

	bResult = TRUE;

cleanup:

	if ( cert )
	{
		X509_free(cert);
	}

	if ( untrusted )
	{
		sk_X509_pop_free(untrusted, X509_free);
	}

	if ( store )
	{
		X509_STORE_free(store);
	}

	if ( csc )
	{
		X509_STORE_CTX_free(csc);
	}

	return bResult;
}

#endif // !defined(WIN32) || defined(USE_OPENSSL)
